<?php
namespace pfp\lib {
    
    \defined('_JEXEC') or die('Restricted access');

    /**
     * Database tablename: tbl_sessions
     * cols:
     *   id               bigint(20)   PrimaryKey
     *   session_id       varchar(255), Unique
     *   date_created     datetime, Default 0000-00-00 00:00:00 
     *   session_data     longtext, Default NULL
     *   timestamp        timestamp, Index, CURRENT_TIMESTAMP 
     */

    /**
     * Description of Session
     *
     * @todo  fix the event names
     *        fingerprint or something else, for more security (eg.to prevent CSRF attacks, etc.)
     * 
     * @author George Zeakis<zeageorge@google.com>
     */
    class Session {

        /**
         * @var DatabaseAdapterInterface
         */
        protected $_db = \NULL;

        /**
         * @var array
         */
        protected $_events = array();

        /**
         *
         * @var array
         */
        protected $_vars = \NULL;

        /**
         * @param deepDynamicContainer $dbData 
         * @param string $session_id Default=NULL
         * @return Session Returns Session object , or throws an codeExecution_FlowControlException(array('return'=>FALSE)) on error
         */
        public function __construct(deepDynamicContainer $dbData, $session_id=\NULL) {
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                $this->registerEvents();
                if (!\session_start()){throw new codeExecution_FlowControlException(array('return'=>\FALSE), "Could not Start Session");}

                $this->_vars = array('dbData'=>$dbData,);
                $this->_vars["session_record"] = new dynamicContainer();

                $temp_session_id = \is_null($session_id) ? (string)\session_id() : (string)$session_id;
                Tools::isValidSessionID($temp_session_id, 26, 128, \TRUE);

                $this->_vars["dbTable"] = $this->_vars["dbData"]->table_prefix().$this->_vars["dbData"]->table_sessions();
                $this->_db = new DatabaseAdapter();

                $temp_array = $this->get_session_record_from_database($temp_session_id);
                foreach ($temp_array as $key => $value) {
                    $this->_vars["session_record"]->{$key} = (string)$value;
                }

                if (!isset($this->_vars["session_record"]->id)) { 
                    $this->create_new_session_record_into_database(array(
                        'session_id'=>$temp_session_id,
                        'date_created'=>\date ("Y-m-d H:i:s", \time()),
                        'session_data'=>(string)\json_encode(array()),
                    ));

                    $temp_array = $this->get_session_record_from_database($temp_session_id);
                    foreach ($temp_array as $key => $value) {
                        $this->_vars["session_record"]->{$key} = (string)$value;
                    }
                    if (!isset($this->_vars["session_record"]->id)) {throw new codeExecution_FlowControlException(array('return'=>\FALSE), "Unknown state. Could not get the new Session data from database.");}
                }
                unset($temp_array, $temp_session_id);

                if (isset($this->_vars["session_record"]->session_data)){ 
                    $this->_vars["session_record"]->session_data =(array)\json_decode($this->_vars["session_record"]->session_data, \TRUE, 512);
                }
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }
        }

        private function registerEvents(){
            $this->_events = array(
                'event_onError'                                          =>'event_onError',
                'event_onBefore_save_session_data_to_database'           =>'event_onBefore_save_session_data_to_database',
                 'event_onAfter_save_session_data_to_database'           => 'event_onAfter_save_session_data_to_database',
                'event_onBefore_delete_session_record_from_database'     =>'event_onBefore_delete_session_record_from_database',
                 'event_onAfter_delete_session_record_from_database'     => 'event_onAfter_delete_session_record_from_database',
                'event_onBefore_get_session_record_from_database'        =>'event_onBefore_get_session_record_from_database',
                 'event_onAfter_get_session_record_from_database'        =>'event_onAfter_get_session_record_from_database',
                'event_onBefore_regenerate_id_into_database'             =>'event_onBefore_regenerate_id_into_database',
                 'event_onAfter_regenerate_id_into_database'             =>'event_onAfter_regenerate_id_into_database',
                'event_onBefore_session_id_exists_in_database'           =>'event_onBefore_session_id_exists_in_database',
                 'event_onAfter_session_id_exists_in_database'           =>'event_onAfter_session_id_exists_in_database',
                'event_onBefore_set_session_id_to_database'              =>'event_onBefore_set_session_id_to_database',
                 'event_onAfter_set_session_id_to_database'              =>'event_onAfter_set_session_id_to_database',
                'event_onBefore_create_new_session_record_into_database' =>'event_onBefore_create_new_session_record_into_database',
                 'event_onAfter_create_new_session_record_into_database' =>'event_onAfter_create_new_session_record_into_database',
            );
            eventManager::registerEvents($this->_events);
        }

        /**
         * @param string $session_id 
         * @param string $session_data_string 
         * @return boolean Returns TRUE on success, or throws an codeExecution_FlowControlException(array('return'=>FALSE),) on error
         */
        private function save_session_data_to_database($session_id, $session_data_string){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                Tools::isValidSessionID($session_id, 26, 128, \TRUE);
                $this->_vars["$tmp"]["sessionData_toSave"] = array('session_data'=>(string)$session_data_string);
                $this->_vars["$tmp"]["session_id"] = (string)$session_id;

                $eventData = array('event_name'=>$this->_events['event_onBefore_save_session_data_to_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);

                $this->_db->connect($this->_vars["dbData"]);
                $this->_vars["$tmp"]["save_session_results"] = 
                $this->_db->update($this->_vars["dbTable"], $this->_vars["$tmp"]["sessionData_toSave"], "`session_id`='".$this->_vars["$tmp"]["session_id"]."'"); 
                $this->_db->disconnect();

                $eventData = array('event_name'=>$this->_events['event_onAfter_save_session_data_to_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]);
                return \TRUE;
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }
        }

        /**
         * It will try to totaly destroy the session....
         * after that, it will regenerate the session_id
         * @param string $session_id
         * @return boolean Returns TRUE on success, or throws an codeExecution_FlowControlException(array('return'=>FALSE),) on error
         */
        private function delete_session_record_from_database($session_id) {
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try{
                Tools::isValidSessionID($session_id, 26, 128, \TRUE);
                $this->_vars["$tmp"]["session_id"] = (string)$session_id;

                $eventData = array('event_name'=>$this->_events['event_onBefore_delete_session_record_from_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);

                @\session_write_close(); @\session_destroy(); @\session_unset(); @\setcookie(\session_name(),'',0,'/'); 
                $_SESSION = array(); unset($_SESSION); @\session_regenerate_id(\true);

                $this->_db->connect($this->_vars["dbData"]);
                $this->_vars["$tmp"]["Delete_Session_Results"] = $this->_db->delete_all($this->_vars["dbTable"], "`id`='".$this->_vars["$tmp"]["session_id"]."'");
                $this->_db->disconnect();

                $eventData = array('event_name'=>$this->_events['event_onAfter_delete_session_record_from_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]);
                return \TRUE;
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }
        }

        /**
         * @param string $session_id 
         * @return array Returns Assoc array with session data
         *               or throws an codeExecution_FlowControlException(array('return'=>FALSE),) on error
         */
        private function get_session_record_from_database($session_id){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                Tools::isValidSessionID($session_id, 26, 128, \TRUE);
                $this->_vars["$tmp"]["sqlQuery"] = "SELECT * FROM `".$this->_vars["dbTable"]."` WHERE `session_id`=:s_id LIMIT 1";
                $this->_vars["$tmp"]["bindParams"] = array(':s_id'=> (string)$session_id);

                $eventData = array('event_name'=>$this->_events['event_onBefore_get_session_record_from_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);

                $this->_db->connect($this->_vars["dbData"]);
                $this->_vars["$tmp"]["select_session_results"] = $this->_db->select($this->_vars["$tmp"]["sqlQuery"], $this->_vars["$tmp"]["bindParams"]);
                $this->_db->disconnect();

                $this->_vars["$tmp"]["return_results"] = array();
                if (\count($this->_vars["$tmp"]["select_session_results"])=='1'){
                    foreach ($this->_vars["$tmp"]["select_session_results"][0] as $key => $value) {
                        $this->_vars["$tmp"]["return_results"]["$key"] = (string)$value;}
                }

                $eventData = array('event_name'=>$this->_events['event_onAfter_get_session_record_from_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                $re = $this->_vars["$tmp"]["return_results"]; unset($this->_vars["$tmp"]);
                return $re;
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }
        }

        /**
         * Regenerates the session_id, it will also update the database and the internal vars
         * 
         * @param boolean $delete_old_session 
         * @return boolean True on success, or throws an codeExecution_FlowControlException(array('return'=>FALSE),) on error
         */
        private function regenerate_id_into_database($delete_old_session=\FALSE){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                $this->_vars["$tmp"]["old_sessionID_of_PHP_Active_Session"] = $this->_vars["session_record"]->session_id;
                $this->_vars["$tmp"]["delete_old_session"] = (boolean)$delete_old_session;

                $eventData = array('event_name'=>$this->_events['event_onBefore_regenerate_id_into_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);

                $found = \TRUE; $num_of_try = 0;
                while ($found) {
                    \session_regenerate_id($this->_vars["$tmp"]["delete_old_session"]);
                    $this->_vars["$tmp"]["new_sessionID_of_PHP_Active_Session"] = \session_id();
                    Tools::isValidSessionID($this->_vars["$tmp"]["new_sessionID_of_PHP_Active_Session"], 26, 128, \TRUE);
                    if ($this->session_id_exists_in_database($this->_vars["$tmp"]["new_sessionID_of_PHP_Active_Session"])){ $num_of_try++; }else{ $found = \FALSE; }
                    if ($num_of_try>5){  //could not regenerate_id
                        // I think, if we want to reset the session_id, the session must be closed ?? (eg. before calling session_start() ??
                        // should I close the session, reset it and then reopen it?
                        \session_id($this->_vars["$tmp"]["old_sessionID_of_PHP_Active_Session"]);
                        unset($this->_vars["$tmp"]); 
                        throw new codeExecution_FlowControlException(array('return'=>\FALSE), "Could not regenerate Session ID");
                    }
                }
                $this->set_session_id_to_database($this->_vars["$tmp"]["new_sessionID_of_PHP_Active_Session"], $this->_vars["$tmp"]["old_sessionID_of_PHP_Active_Session"]);

                $eventData = array('event_name'=>$this->_events['event_onAfter_regenerate_id_into_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]);
                return \TRUE;
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }
        }

        /**
         * 
         * @param string $session_id
         * @return boolean Returns TRUE if $session_id already exists in database, or FALSE if it doesn't exists, or throws an codeExecution_FlowControlException(array('return'=>FALSE),) on error
         */
        private function session_id_exists_in_database($session_id){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                $this->_vars["$tmp"]["session_id_toCheck"] = (string)$session_id;
                $this->_vars["$tmp"]["sqlQuery"]   = "SELECT COUNT(session_id) AS C FROM `".$this->_vars["dbTable"]."` WHERE `session_id`=:s_id LIMIT 1";
                $this->_vars["$tmp"]["bindParams"] = array(':s_id'=>$this->_vars["$tmp"]["session_id_toCheck"]);

                $eventData = array('event_name'=>$this->_events['event_onBefore_session_id_exists_in_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);

                $this->_db->connect($this->_vars["dbData"]);
                $this->_vars["$tmp"]["session_id_exists_selectResults"] = $this->_db->select($this->_vars["$tmp"]["sqlQuery"], $this->_vars["$tmp"]["bindParams"]);
                $this->_db->disconnect();

                if (isset($this->_vars["$tmp"]["session_id_exists_selectResults"][0]['C'])){
                    if ($this->_vars["$tmp"]["session_id_exists_selectResults"][0]['C']=='0')
                        { $this->_vars["$tmp"]["session_id_exists_Results"] = \FALSE; } else { $this->_vars["$tmp"]["session_id_exists_Results"] = \TRUE;}
                } else { 
                    throw new codeExecution_FlowControlException(array('return'=>\FALSE), "Unknown state: Could not check if Session ID exists in Database"); 
                }

                $eventData = array('event_name'=>$this->_events['event_onAfter_session_id_exists_in_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);

                $re = $this->_vars["$tmp"]["session_id_exists_Results"];
                unset($this->_vars["$tmp"]);
                return $re;
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }
        }

        /**
         * Updates the database with the new session_id (session_id)
         * 
         * @param type $new_session_id
         * @param type $old_session_id
         * @return boolean TRUE on success or throws an codeExecution_FlowControlException(array('return'=>FALSE),) on error
         */
        private function set_session_id_to_database($new_session_id, $old_session_id){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                $this->_vars["$tmp"]["new_session_id"] = $new_session_id;
                $this->_vars["$tmp"]["old_session_id"] = $old_session_id;

                $eventData = array('event_name'=>$this->_events['event_onBefore_set_session_id_to_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);

                $this->_db->connect($this->_vars["dbData"]);
                $this->_vars["$tmp"]["set_new_session_id_Results"] = 
                $this->_db->update($this->_vars["dbTable"],array('session_id'=>$this->_vars["$tmp"]["new_session_id"]), "`session_id`='".$this->_vars["$tmp"]["old_session_id"]."'");
                $this->_db->disconnect();

                $eventData = array('event_name'=>$this->_events['event_onAfter_set_session_id_to_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]);
                return \TRUE;
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }
        }

        /**
         * @param array $new_session_record_data An array like: 
         *                                           array(
         *                                               'session_id'=>$temp_session_id,
         *                                               'date_created'=>date ("Y-m-d H:i:s", time()),
         *                                               'session_data'=>(string)json_encode(array()),
         *                                           )
         * @return boolean Returns TRUE on success or throws an codeExecution_FlowControlException(array('return'=>FALSE),) on error
         */
        private function create_new_session_record_into_database(array $new_session_record_data){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                $this->_vars["$tmp"]["dataToInsert"] = $new_session_record_data; 

                $eventData = array('event_name'=>$this->_events['event_onBefore_create_new_session_record_into_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);

                $this->_db->connect($this->_vars["dbData"]);
                $this->_vars["$tmp"]["insert_results"] = $this->_db->insert($this->_vars["dbTable"], $this->_vars["$tmp"]["dataToInsert"]);
                $this->_db->disconnect();

                $eventData = array('event_name'=>$this->_events['event_onAfter_create_new_session_record_into_database'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]);
                return \TRUE;
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }    
        }

        public function __get($name){
            if(isset($this->_vars["session_record"]->session_data[$name])){
              return $this->_vars["session_record"]->session_data[$name];
            } else { 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE),"SessionRecord variable $name does not exists");
            }
        }
        public function __set($name,$value){ 
            // the following must be this way,
            // because we get an error: PHP Notice: Indirect modification of overloaded property... 
            // if we do it the "normal" way, eg. $this->_vars["session_record"]->session_data[$name] = $value;
            $yu = $this->_vars["session_record"]->session_data; 
            $yu[$name] = $value;
            $this->_vars["session_record"]->session_data = $yu; 
        }
        public function __isset($name){ return isset($this->_vars["session_record"]->session_data[$name]); }
        public function __unset($name){ 
            // the following must be this way,
            // because we get an error: PHP Notice: Indirect modification of overloaded property... 
            // if we do it the "normal" way, eg. unset($this->_vars["session_record"]->session_data[$name]);
            $yu = $this->_vars["session_record"]->session_data;
            unset($yu[$name]);
            $this->_vars["session_record"]->session_data = $yu; 
        }

        public function save(){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                if (!isset($this->_vars["session_record"]->session_id)){
                    throw new codeExecution_FlowControlException(array('return'=>\FALSE), 
                            "Could not save session data into database, session id is not defined");
                }

                $this->save_session_data_to_database($this->_vars["session_record"]->session_id, 
                        (string)\json_encode($this->_vars["session_record"]->session_data));

            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }    
        }

        public function delete(){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                if (!isset($this->_vars["session_record"]->session_id)){
                    throw new codeExecution_FlowControlException(array('return'=>\FALSE), 
                            "Could not delete session from database, session id is not defined");
                }
                $this->delete_session_record_from_database($this->_vars["session_record"]->session_id);
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }    
        }

        public function regenerate_id($delete_old_session){
            $tmp = __FUNCTION__."_tmp"; if (isset($this->_vars["$tmp"])){ unset($this->_vars["$tmp"]); }
            try {
                if (!isset($this->_vars["session_record"]->session_id)){
                    throw new codeExecution_FlowControlException(array('return'=>\FALSE), 
                            "Could not regenerate session id, session id is not defined");
                }
                $this->regenerate_id_into_database($delete_old_session);
            } catch (codeExecution_FlowControlException $exc) {
                $this->_vars["$tmp"]["errors"] = $exc;
                $eventData = array('event_name'=>$this->_events['event_onError'],'args'=>&$this->_vars); eventManager::riseEvent($eventData);
                unset($this->_vars["$tmp"]); 
                throw new codeExecution_FlowControlException(array('return'=>\FALSE), $exc->getMessage());
            }    
        }

        public function sessionRecord_toArray() {
            return (array)$this->_vars["session_record"]->toArray();
        }

        public function session_data_toArray(){
            return (array)$this->_vars["session_record"]->session_data;
        }

        public function __destruct() { }

    }
}